﻿
namespace Universe.MediaProbe
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Globalization;
    using System.IO;

    public class AVProbeParser
    {
        static readonly CultureInfo EnUs = new CultureInfo("en-US");
        private static readonly decimal UNKNOWN_DECIMAL = 0m;
        private static readonly int UNKNOWN_INT = 0;

        public static ProbeReport Parse(string avprobeOutput)
        {
            // Trace.WriteLine("Parse(string avprobeOutput)" + Environment.NewLine + avprobeOutput);
            StringReader rdr = new StringReader(avprobeOutput);
            List<SectionInfo> sections = new List<SectionInfo>(EnumSections(rdr));
            var formats = Find(sections, Section.Format);
            var format = formats.Count > 0 ? formats[0] : null;
            if (format == null)
                throw new Exception("Media format (e.g. section [FORMAT]) is undefined");

            ProbeReport ret = new ProbeReport()
            {
                Format = TryString(format, "format_name"),
                FormatLongName = TryString(format, "format_long_name"),
                Duration = TrySimpleDecimal3(format, "duration"),
                BitRate = TrySimpleDecimal3(format, "bit_rate"),
            };

            var streams = Find(sections, Section.Stream);
            foreach (var stream in streams)
            {
                var streamType = TryString(stream, "codec_type");
                string codecName = TryString(stream, "codec_name");
                string codecLongName = TryString(stream, "codec_long_name");
                int? idStream = TryInt(stream, "index");

                if (!idStream.HasValue) continue;

                if ("Video".Equals(streamType, StringComparison.InvariantCultureIgnoreCase))
                {
                    var avgFrameRate = TryFrameRate(TryString(stream, "avg_frame_rate"));
                    var frameRate = TryFrameRate(TryString(stream, "r_frame_rate"));
                    ret.Videos.Add(new ProbeReport.VideoInfo()
                    {
                        IdStream = idStream.Value,
                        Codec = codecName,
                        CodecLongName = codecLongName,
                        FrameRate = avgFrameRate == 0 ? frameRate : avgFrameRate,
                        Height = TrySimpleInt(stream, "height"),
                        Width = TrySimpleInt(stream, "width"),
                    });
                }

                if ("Audio".Equals(streamType, StringComparison.InvariantCultureIgnoreCase))
                {
                    ret.Audios.Add(new ProbeReport.AudioInfo
                    {
                        IdStream = idStream.Value,
                        Codec = codecName,
                        CodecLongName = codecLongName,
                        BitRate = TrySimpleInt(stream, "bit_rate"),
                        Channels = TrySimpleInt(stream, "channels"),
                        Language = TryString(stream, "TAG:LANGUAGE"),
                        SampleRate = TrySimpleInt(stream, "sample_rate"),
                    });
                }

                if ("Subtitle".Equals(streamType, StringComparison.InvariantCultureIgnoreCase))
                {
                    ret.Subs.Add(new ProbeReport.SubInfo
                    {
                        IdStream = idStream.Value,
                        Codec = codecName,
                        CodecLongName = codecLongName,
                        Language = TryString(stream, "TAG:LANGUAGE"),
                    });
                }

            }

            return ret;
        }

        static int TrySimpleInt(SectionInfo section, string name)
        {
            var ret = TryInt(section, name);
            return ret.HasValue ? ret.Value : UNKNOWN_INT;
        }


        static string TryString(SectionInfo section, string name)
        {
            foreach (var row in section.Content)
            {
                if (row.Key.Equals(name, StringComparison.InvariantCultureIgnoreCase))
                    return row.Value;
            }

            return null;
        }

        // raw: 23.297 | 24000/1001
        private static decimal TryFrameRate(string raw)
        {
            if (raw == null)
                return UNKNOWN_DECIMAL;

            decimal result;
            if (decimal.TryParse(raw, NumberStyles.Any | NumberStyles.AllowDecimalPoint, EnUs, out result))
                return result;

            var arr = raw.Split('/');
            if (arr.Length == 2)
            {
                decimal d1, d2;
                if (decimal.TryParse(arr[0], NumberStyles.Any, EnUs, out d1))
                    if (decimal.TryParse(arr[1], NumberStyles.Any, EnUs, out d2))
                        if (d2 != 0)
                            return CrossInfo.Round(d1 / d2, 3);
            }

            return UNKNOWN_DECIMAL;
        }


        static int? TryInt(SectionInfo section, string name)
        {
            var raw = TryString(section, name);
            if (string.IsNullOrEmpty(raw))
                return null;

            int result;
            if (int.TryParse(raw, NumberStyles.Any, EnUs, out result))
                return result;

            return null;
        }


        static decimal TrySimpleDecimal(SectionInfo section, string name)
        {
            var raw = TryString(section, name);
            decimal result;
            if (decimal.TryParse(raw, NumberStyles.Any | NumberStyles.AllowDecimalPoint, EnUs, out result))
                return result;

            return UNKNOWN_DECIMAL;
        }

        
        static decimal TrySimpleDecimal3(SectionInfo section, string name)
        {

            var raw_str = TryString(section, name);
            decimal raw_dec = TrySimpleDecimal(section, name);
            try
            {
                return CrossInfo.Round(raw_dec, 4);
            }
            catch (Exception ex)
            {
                throw new Exception("Rounding decimal [" + raw_dec + "] with string [" + raw_str + "] at key " + name + " failed", ex);
            }
        }


        static List<SectionInfo> Find(List<SectionInfo> sections, Section kind)
        {
            List<SectionInfo> ret = new List<SectionInfo>();
            foreach (var s in sections)
                if (kind == s.Section)
                    ret.Add(s);

            return ret;
        }

        private delegate T _Function<T>();

        public static IEnumerable<SectionInfo> EnumSections(StringReader reader)
        {
            List<AVInfoRow> buffer = new List<AVInfoRow>();
            var tmp = new List<AVInfoRow>(ParseLines(reader));
            foreach (var row in tmp)
            {
                if (buffer.Count > 0 && row.SectionIndex != buffer[0].SectionIndex)
                {
                    yield return new SectionInfo()
                    {
                        Content = new List<AVInfoRow>(buffer),
                        Section = buffer[0].Section,
                        SectionRaw = buffer[0].SectionRaw,
                    };

                    buffer.Clear();
                }

                buffer.Add(row);
            }
         
            if (buffer.Count > 0)
                yield return new SectionInfo()
                {
                    Content = new List<AVInfoRow>(buffer),
                    Section = buffer[0].Section,
                    SectionRaw = buffer[0].SectionRaw,
                };
        }

        public static IEnumerable<AVInfoRow> ParseLines(StringReader reader)
        {
            string line;
            int sectionIndex = 0;
            string sectionRaw = null;
            Section section = Section.Other;
            StringComparison comp = StringComparison.InvariantCultureIgnoreCase;
            while ((line = reader.ReadLine()) != null)
            {
                if ("[STREAM]".Equals(line, comp))
                {
                    sectionRaw = "STREAM";
                    section = Section.Stream;
                    sectionIndex++;
                }
                else if ("[FORMAT]".Equals(line, comp))
                {
                    sectionRaw = "FORMAT";
                    section = Section.Format;
                    sectionIndex++;
                }
                else if (line.StartsWith("[") && line.EndsWith("]") && line.Length > 2)
                {
                    string s = line.Substring(1, line.Length - 2);
                    if (!s.StartsWith("/"))
                    {
                        sectionRaw = s;
                        section = Section.Other;
                        sectionIndex++;
                    }
                }
                else if (!line.StartsWith("[") /* && !line.EndsWith("]") */)
                {
                    int pos = line.IndexOf("=", comp);
                    if (pos > 0)
                    {
                        string key = line.Substring(0, pos);
                        string value = pos == line.Length - 1 ? "" : line.Substring(pos + 1, line.Length - 1 - pos);
                        yield return new AVInfoRow()
                        {
                            SectionIndex = sectionIndex,
                            Section = section,
                            SectionRaw = sectionRaw,
                            Key = key,
                            Value = value,
                        };

                    }
                }
            }
        }

        public class AVInfoRow
        {
            public int SectionIndex;
            public Section Section;
            public string SectionRaw;
            public string Key;
            public string Value;
        }

        public enum Section
        {
            Format,
            Stream,
            Other,
        }

        public class SectionInfo
        {
            public Section Section;
            public string SectionRaw;
            public List<AVInfoRow> Content;
        }



    }
}
